1 module ffeedd.feed; 2 3 import std.datetime; 4 import ffeedd.author; 5 import ffeedd.item; 6 import vibe.textfilter.html; 7 import std.conv; 8 9 /// Based on: 10 /// RSS: https://www.rssboard.org/rss-specification 11 /// ATOM: https://www.tutorialspoint.com/rss/feed.htm 12 class Feed 13 { 14 /// Instances a new channel only with the required elements 15 this(string feedTitle, string feedLink, string feedDescription) 16 { 17 title = feedTitle; 18 link = feedLink; 19 description = feedDescription; 20 } 21 22 /// The name of the feed. It's how people refer to your service. 23 /// e.g. "GoUpstate.com News Headlines" 24 string title; 25 /// The URL to the HTML website corresponding to the channel. 26 /// eg. "http://www.goupstate.com/" 27 string link; 28 /// Phrase or sentence describing the channel. 29 /// e.g. "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site." 30 string description; 31 /// Items in this feed 32 FeedItem[] items; 33 34 // Optional elements 35 /// Time when the feed was last updated 36 DateTime updated; 37 /// This contains the time of the initial creation or the first availability of the entry. 38 DateTime published; 39 /// List of authors 40 Author[] authors; 41 42 string createAtomFeed() 43 { 44 string s = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n"; 45 46 if (title) 47 s ~= "\t<title>" ~ htmlAttribEscape(title) ~ "</title>\n"; 48 49 if (link) 50 s ~= "\t<id>" ~ htmlAttribEscape(link) ~ "</id>\n"; 51 52 if (description) 53 s ~= "\t<subtitle>" ~ htmlAttribEscape(link) ~ "</subtitle>\n"; 54 55 if (updated != DateTime()) 56 s ~= "\t<updated>" ~ toRFC3339String(updated) ~ "</updated>\n"; 57 58 // Stops w3.org checker from validating 59 // tutorialspoint.com probably made a mistake and included <published> in the feed 60 61 // if (published != DateTime()) 62 // s ~= "\t<published>" ~ toRFC3339String(updated) ~ "</published>\n"; 63 64 foreach (author; authors) 65 { 66 s ~= "\t<author>\n"; 67 68 if (author.name) 69 s ~= "\t\t<name>" ~ htmlAttribEscape(author.name) ~ "</name>\n"; 70 71 if (author.email) 72 s ~= "\t\t<email>" ~ htmlAttribEscape(author.email) ~ "</email>\n"; 73 74 s ~= "\t</author>\n"; 75 } 76 77 foreach (item; items) 78 { 79 s ~= "\t<entry>\n"; 80 81 if (item.title) 82 s ~= "\t\t<title>" ~ htmlAttribEscape(item.title) ~ "</title>\n"; 83 84 if (item.link) 85 { 86 s ~= "\t\t<id>" ~ htmlAttribEscape(item.link) ~ "</id>\n"; 87 s ~= "\t\t<link rel=\"alternate\" href=\"" ~ htmlAttribEscape(item.link) ~ "\"/>\n"; 88 } 89 90 if (item.description) 91 s ~= "\t\t<summary>" ~ htmlAttribEscape(item.description) ~ "</summary>\n"; 92 93 foreach (author; item.authors) 94 { 95 s ~= "\t\t<author>\n"; 96 97 if (author.name) 98 s ~= "\t\t\t<name>" ~ htmlAttribEscape(author.name) ~ "</name>\n"; 99 100 if (author.email) 101 s ~= "\t\t\t<email>" ~ htmlAttribEscape(author.email) ~ "</email>\n"; 102 103 s ~= "\t\t</author>\n"; 104 } 105 106 if (item.updated != DateTime()) 107 s ~= "\t\t<updated>" ~ toRFC3339String(item.updated) ~ "</updated>\n"; 108 109 if (item.published != DateTime()) 110 s ~= "\t\t<published>" ~ toRFC3339String(item.published) ~ "</published>\n"; 111 112 113 s ~= "\t</entry>\n"; 114 } 115 116 s ~= "</feed>"; 117 118 return s; 119 } 120 121 string createRSSFeed() 122 { 123 string s = "<rss version=\"2.0\"> 124 <channel>\n"; 125 126 if (title) 127 s ~= "\t<title>" ~ htmlAttribEscape(title) ~ "</title>\n"; 128 129 if (link) 130 s ~= "\t<link>" ~ htmlAttribEscape(link) ~ "</link>\n"; 131 132 if (description) 133 s ~= "\t<description>" ~ htmlAttribEscape(link) ~ "</description>\n"; 134 135 if (updated != DateTime()) 136 s ~= "\t<lastBuildDate>" ~ toRFC822String(updated) ~ "</lastBuildDate>\n"; 137 138 if (published != DateTime()) 139 s ~= "\t<pubDate>" ~ toRFC822String(updated) ~ "</pubDate>\n"; 140 141 // RSS 2.0 feed doesn't have author tags 142 // but it doesn't break anything so I'm keeping it 143 foreach (author; authors) 144 s ~= "\t<author>" ~ htmlAttribEscape(author.email) ~ " (" ~ htmlAttribEscape(author.name) ~ ")</author>\n"; 145 146 foreach (item; items) 147 { 148 s ~= "\t\t<item>\n"; 149 150 if (item.title) 151 s ~= "\t\t\t<title>" ~ htmlAttribEscape(item.title) ~ "</title>\n"; 152 153 if (item.link) 154 { 155 s ~= "\t\t\t<link>" ~ htmlAttribEscape(item.link) ~ "</link>\n"; 156 s ~= "\t\t\t<guid isPermaLink=\"true\">" ~ htmlAttribEscape(item.link) ~ "</guid>\n"; 157 } 158 159 if (item.description) 160 s ~= "\t\t\t<description>" ~ htmlAttribEscape(item.description) ~ "</description>\n"; 161 162 foreach (author; authors) 163 s ~= "\t\t\t<author>" ~ htmlAttribEscape(author.email) ~ " (" ~ htmlAttribEscape(author.name) ~ ")</author>\n"; 164 165 if (item.published != DateTime()) 166 s ~= "\t\t\t<pubDate>" ~ toRFC822String(item.published) ~ "</pubDate>\n"; 167 168 169 s ~= "\t\t</item>\n"; 170 } 171 172 s ~= "\t</channel>\n</rss>"; 173 174 return s; 175 } 176 } 177 178 /// Convert DateTime to RFC 3339 date-time string 179 /// e.g. 2002-10-02T10:00:00Z 180 string toRFC3339String(DateTime date) 181 { 182 return date.toISOExtString() ~ "Z"; 183 } 184 185 /// Convert DateTime to RFC 822 date-time string 186 /// Sat, 07 Sep 2002 00:00:01 GMT 187 string toRFC822String(DateTime date) 188 { 189 import std.range; 190 import std.string; 191 import std.format; 192 193 string day = date.dayOfWeek.to!string(); 194 //day[0] = day[0].toUpper(); 195 196 string month = date.month.to!string(); 197 //month[0].toUpper()[0]; 198 199 return day ~ ", " ~ date.day.to!string().padLeft('0', 2).text ~ " " ~ month ~ " " ~ date.year.to!string() ~ " " ~ 200 date.hour.to!string().padLeft('0', 2).text ~ ":" ~ 201 date.minute.to!string().padLeft('0', 2).text ~ ":" ~ 202 date.second.to!string().padLeft('0', 2).text ~ " +0000"; 203 }