1 module pastemyst.paste;
2 
3 import std.typecons;
4 import std.uri;
5 import vibe.d;
6 import pastemyst.info;
7 import pastemyst.expires;
8 
9 /++
10  + holds information about a single paste edit
11  +/
12 public struct Edit
13 {
14     /++
15      + unique id of the edit
16      +/
17     @name("_id")
18     public string uniqueId;
19 
20     /++
21      + edit id, multiple edits can share the same id to show that multiple properties were edited at the same time
22      +/
23     public ulong editId;
24 
25     /++
26      + type of edit
27      +/
28     public EditType editType;
29 
30     /++
31      + various metadata, most used case is for storing which pasty was edited
32      +/
33     public string[] metadata;
34 
35     /++
36      + actual edit, usually stores the old data
37      +/
38     public string edit;
39 
40     /++
41      + unix time of when the edit was done
42      +/
43     public ulong editedAt;
44 }
45 
46 /++
47  + type of paste edit
48  +/
49 public enum EditType
50 {
51     title,
52     pastyTitle,
53     pastyLanguage,
54     pastyContent,
55     pastyAdded,
56     pastyRemoved,
57 }
58 
59 /++
60  + struct for a single pasty. a pasty is a part of a paste and represents a single "file", contains a title, language and code.
61  +/
62 public struct Pasty
63 {
64     /++
65      + id of the pasty
66      +/
67     @name("_id")
68     @optional
69     public string id;
70 
71     /++
72      + title of the pasty.
73      +/
74     public string title;
75 
76     /++
77      + language of the pasty. this stores the name of the language, not the mode or MIME type.
78      +/
79     public string language;
80 
81     /++
82      + code of the pasty.
83      +/
84     public string code;
85 }
86 
87 /++
88  + struct representing a paste.
89  +/
90 public struct Paste
91 {
92     /++
93      + paste id
94      +/
95     @name("_id")
96     public string id;
97 
98     /++
99      + when the paste is created, using unix time.
100      +/
101     public ulong createdAt;
102 
103     /++
104      + when the paste expires.
105      +/
106     public ExpiresIn expiresIn;
107 
108     /++
109      + when the paste will get deleted, if `expiresIn` is set to never, this value is set to 0;
110      +/
111     public ulong deletesAt;
112 
113     /++
114      + owner of the paste. if no owner then this value should be an empty string.
115      +/
116     public string ownerId;
117 
118     /++
119      + if the paste is private.
120      +/
121     public bool isPrivate;
122 
123     /++
124      + does the paste show up on the user's public profile?
125      +/
126     public bool isPublic;
127 
128     /++
129      + array of all tags for this paste
130      +/
131     public string[] tags;
132 
133     /++
134      + number of stars
135      +/
136     public ulong stars;
137 
138     /++
139      + is the paste encrytped?
140      +/
141     public bool encrypted;
142 
143     /++
144      + title of the paste.
145      +/
146     public string title;
147 
148     /++
149      + pasties of the paste. a paste can have multiple pasties which are sort of like "files".
150      +/
151     public Pasty[] pasties;
152 
153     /++
154      + array of paste edits
155      +/
156     public Edit[] edits;
157 }
158 
159 /++
160  + struct containing info needed to create a pasty
161  +/
162 public struct PastyCreateInfo
163 {
164     /++
165      + title of the pasty.
166      +/
167     public string title;
168 
169     /++
170      + language of the pasty. this stores the name of the language, not the mode or MIME type.
171      +/
172     public string language;
173 
174     /++
175      + code of the pasty.
176      +/
177     public string code;
178 }
179 
180 /++
181  + struct containing info needed to create a paste
182  +/
183 public struct PasteCreateInfo
184 {
185     /++
186      + title -- optional
187      +/
188     public string title;
189 
190     /++
191      + expires in -- optional
192      +/
193     public ExpiresIn expiresIn;
194 
195     /++
196      + is it only accessible by the owner -- optional
197      +/
198     public bool isPrivate;
199 
200     /++
201      + is it displayed on the owners public profile -- optional
202      +/
203     public bool isPublic;
204 
205     /++
206      + tags, comma separated -- optional
207      +/
208     public string tags;
209 
210     /++
211      + list of pasties -- mandatory
212      +/
213     public PastyCreateInfo[] pasties;
214 }
215 
216 /++
217  + returns a paste if it can find it by its id
218  +
219  + if the paste is private you need to provide the token (found on your profile's settings page)
220  +/
221 public Nullable!Paste getPaste(string id, string token = "")
222 {
223     Nullable!Paste paste = Nullable!Paste.init;
224 
225     requestHTTP(PASTE_ENDPOINT ~ id,
226         (scope req)
227         {
228             req.method = HTTPMethod.GET;
229 
230             if (token != "")
231             {
232                 req.headers.addField("Authorization", token);
233             }
234         },
235         (scope res)
236         {
237             if (res.statusCode != HTTPStatus.notFound)
238             {
239                 try
240                 {
241                     paste = nullable(deserializeJson!Paste(res.bodyReader.readAllUTF8()));
242                 } catch (Exception) {}
243             }
244         }
245     );
246 
247     return paste;
248 }
249 
250 /++
251  + creates a paste and returns the full paste info
252  +
253  + if you want the paste to be tied to your account or to create a private/public paste, or use other account specific features you have to provide the token.
254  +/
255 public Paste createPaste(const PasteCreateInfo createInfo, string token = "")
256 {
257     Paste result = Paste.init;
258 
259     requestHTTP(BASE_ENDPOINT ~ "paste",
260         (scope req)
261         {
262             req.method = HTTPMethod.POST;
263             req.headers.addField("Content-Type", "application/json");
264 
265             if ((createInfo.isPrivate || createInfo.isPublic || createInfo.tags != "") && token == "")
266             {
267                 throw new Exception("using account features but the token isnt provided");
268             }
269 
270             if (token != "")
271             {
272                 req.headers.addField("Authorization", token);
273             }
274 
275             req.writeJsonBody(createInfo);
276         },
277         (scope res)
278         {
279             try
280             {
281                 result = nullable(deserializeJson!Paste(res.bodyReader.readAllUTF8()));
282             } catch (Exception) {}
283         }
284     );
285 
286     return result;
287 }
288 
289 /++
290  + deletes a paste
291  +
292  + you can only delete pastes on your account so you must provide the token
293  +
294  + this action is irreversible
295  +/
296 public void deletePaste(string id, string token)
297 {
298     requestHTTP(PASTE_ENDPOINT ~ id,
299         (scope req)
300         {
301             req.method = HTTPMethod.DELETE;
302             req.headers.addField("Content-Type", "application/json");
303 
304             req.headers.addField("Authorization", token);
305         }, (scope res) {}
306     );
307 }
308 
309 /++
310  + edits a paste
311  +
312  + you can only edit pastes on your account so you must provide the token
313  +
314  + returns the new edited paste
315  +
316  + to edit values you need to send the exact same paste, except values you want edited should be changed
317  +
318  + you cant edit the expires in value, changing it will have no effect
319  +/
320 public Paste editPaste(Paste paste, string token)
321 {
322     Paste result = Paste.init;
323 
324     requestHTTP(PASTE_ENDPOINT ~ paste.id,
325         (scope req)
326         {
327             req.method = HTTPMethod.PATCH;
328             req.headers.addField("Content-Type", "application/json");
329 
330             req.headers.addField("Authorization", token);
331 
332             req.writeJsonBody(paste);
333         },
334         (scope res)
335         {
336             try
337             {
338                 result = nullable(deserializeJson!Paste(res.bodyReader.readAllUTF8()));
339             } catch (Exception) {}
340         }
341     );
342 
343     return result;
344 }
345 
346 @("getting a paste")
347 unittest
348 {
349     const paste = getPaste("cwy615yg").get();
350 
351     assert(paste.title == "DONT DELETE - api example");
352 }
353 
354 @("creating a paste")
355 unittest
356 {
357     const pastyCreateInfo = PastyCreateInfo("pasty1", "plain text", "asd asd asd");
358 
359     const createInfo = PasteCreateInfo("api test paste",
360             ExpiresIn.never,
361             false,
362             false,
363             "",
364             [pastyCreateInfo]);
365 
366     const paste = createPaste(createInfo);
367 
368     assert(paste.title == createInfo.title);
369 }
370 
371 @("creating a private paste")
372 unittest
373 {
374     import std.process : environment;
375 
376     const token = environment.get("TOKEN");
377 
378     const pastyCreateInfo = PastyCreateInfo("pasty1", "plain text", "asd asd asd");
379 
380     const createInfo = PasteCreateInfo("api test paste",
381             ExpiresIn.never,
382             true,
383             false,
384             "",
385             [pastyCreateInfo]);
386 
387     const paste = createPaste(createInfo, token);
388 
389     assert(paste.isPrivate);
390 }
391 
392 @("deleting a paste")
393 unittest
394 {
395     import std.process : environment;
396 
397     const token = environment.get("TOKEN");
398 
399     const pastyCreateInfo = PastyCreateInfo("pasty1", "plain text", "asd asd asd");
400 
401     const createInfo = PasteCreateInfo("api test paste",
402             ExpiresIn.never,
403             false,
404             false,
405             "",
406             [pastyCreateInfo]);
407 
408     const paste = createPaste(createInfo, token);
409 
410     deletePaste(paste.id, token);
411 
412     assert(getPaste(paste.id, token).isNull());
413 }
414 
415 @("editing a paste")
416 unittest
417 {
418     import std.process : environment;
419 
420     const token = environment.get("TOKEN");
421 
422     const pastyCreateInfo = PastyCreateInfo("pasty1", "plain text", "asd asd asd");
423 
424     const createInfo = PasteCreateInfo("api test paste",
425             ExpiresIn.never,
426             false,
427             false,
428             "",
429             [pastyCreateInfo]);
430 
431     auto paste = createPaste(createInfo, token);
432 
433     paste.title = "edited title";
434 
435     editPaste(paste, token);
436 }