diff --git a/contrib/frontends/cpp/nntpchan-daemon/.gitignore b/contrib/frontends/cpp/nntpchan-daemon/.gitignore new file mode 100644 index 0000000..299a962 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/.gitignore @@ -0,0 +1,2 @@ +*.o +nntpchan \ No newline at end of file diff --git a/contrib/frontends/cpp/nntpchan-daemon/Makefile b/contrib/frontends/cpp/nntpchan-daemon/Makefile new file mode 100644 index 0000000..530acd7 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/Makefile @@ -0,0 +1,29 @@ + +EXE = nntpchan + +CXX = clang++ + +SRC_PATH = ./src + +SOURCES := $(wildcard $(SRC_PATH)/*.cpp) +HEADERS := $(wildcard $(SRC_PATH)/*.hpp) +OBJECTS := $(SOURCES:.cpp=.o) + +PKGS := libuv + +LD_FLAGS := $(shell pkg-config --libs $(PKGS)) +INC_FLAGS := -I $(SRC_PATH) $(shell pkg-config --cflags $(PKGS)) +CXXFLAGS := -std=c++14 -Wall -Wextra $(INC_FLAGS) + + + +all: $(EXE) + +$(EXE): $(OBJECTS) + $(CXX) -o $(EXE) $(LD_FLAGS) $(OBJECTS) + +%.o: src/%.cpp + $(CXX) $(CXXFLAGS) -c -o $@ + +clean: + rm -f $(OBJECTS) $(EXE) diff --git a/contrib/frontends/cpp/nntpchan-daemon/nntpchan.ini b/contrib/frontends/cpp/nntpchan-daemon/nntpchan.ini new file mode 100644 index 0000000..3b50894 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/nntpchan.ini @@ -0,0 +1,6 @@ +[nntp] +bind = [::]:1199 + + +[storage] +path = ./storage/ \ No newline at end of file diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/buffer.cpp b/contrib/frontends/cpp/nntpchan-daemon/src/buffer.cpp new file mode 100644 index 0000000..938cc7f --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/buffer.cpp @@ -0,0 +1,21 @@ +#include "buffer.hpp" +#include + +namespace nntpchan +{ + WriteBuffer::WriteBuffer(const char * b, const size_t s) + { + char * buf = new char[s]; + std::memcpy(buf, b, s); + this->b = uv_buf_init(buf, s); + w.data = this; + }; + + WriteBuffer::WriteBuffer(const std::string & s) : WriteBuffer(s.c_str(), s.size()) {} + + WriteBuffer::~WriteBuffer() + { + delete [] b.base; + } +} + diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/buffer.hpp b/contrib/frontends/cpp/nntpchan-daemon/src/buffer.hpp new file mode 100644 index 0000000..be9beee --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/buffer.hpp @@ -0,0 +1,19 @@ +#ifndef NNTPCHAN_BUFFER_HPP +#define NNTPCHAN_BUFFER_HPP +#include +#include + +namespace nntpchan +{ + struct WriteBuffer + { + uv_write_t w; + uv_buf_t b; + + WriteBuffer(const std::string & s); + WriteBuffer(const char * b, const size_t s); + ~WriteBuffer(); + }; +} + +#endif diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/event.cpp b/contrib/frontends/cpp/nntpchan-daemon/src/event.cpp new file mode 100644 index 0000000..b269816 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/event.cpp @@ -0,0 +1,27 @@ +#include "event.hpp" +#include + +namespace nntpchan +{ + Mainloop::Mainloop() + { + m_loop = uv_default_loop(); + assert(uv_loop_init(m_loop) == 0); + } + + Mainloop::~Mainloop() + { + uv_loop_close(m_loop); + } + + void Mainloop::Stop() + { + uv_stop(m_loop); + } + + void Mainloop::Run(uv_run_mode mode) + { + uv_run(m_loop, mode); + } + +} diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/event.hpp b/contrib/frontends/cpp/nntpchan-daemon/src/event.hpp new file mode 100644 index 0000000..1bd1acf --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/event.hpp @@ -0,0 +1,26 @@ +#ifndef NNTPCHAN_EVENT_HPP +#define NNTPCHAN_EVENT_HPP +#include + +namespace nntpchan +{ + class Mainloop + { + public: + + Mainloop(); + ~Mainloop(); + + operator uv_loop_t * () const { return m_loop; } + + void Run(uv_run_mode mode = UV_RUN_DEFAULT); + void Stop(); + + private: + + uv_loop_t * m_loop; + + }; +} + +#endif diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/ini.hpp b/contrib/frontends/cpp/nntpchan-daemon/src/ini.hpp new file mode 100644 index 0000000..724e346 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/ini.hpp @@ -0,0 +1,186 @@ +/** + * The MIT License (MIT) + * Copyright (c) <2015> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef INI_HPP +#define INI_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace INI { + +struct Level +{ + Level() : parent(NULL), depth(0) {} + Level(Level* p) : parent(p), depth(0) {} + + typedef std::map value_map_t; + typedef std::map section_map_t; + typedef std::list values_t; + typedef std::list sections_t; + value_map_t values; + section_map_t sections; + values_t ordered_values; // original order in the ini file + sections_t ordered_sections; + Level* parent; + size_t depth; + + const std::string& operator[](const std::string& name) { return values[name]; } + Level& operator()(const std::string& name) { return sections[name]; } +}; + +class Parser +{ +public: + Parser(const char* fn); + Parser(std::istream& f) : f_(&f), ln_(0) { parse(top_); } + Level& top() { return top_; } + void dump(std::ostream& s) { dump(s, top(), ""); } + +private: + void dump(std::ostream& s, const Level& l, const std::string& sname); + void parse(Level& l); + void parseSLine(std::string& sname, size_t& depth); + void err(const char* s); + +private: + Level top_; + std::ifstream f0_; + std::istream* f_; + std::string line_; + size_t ln_; +}; + +inline void +Parser::err(const char* s) +{ + char buf[256]; + sprintf(buf, "%s on line #%ld", s, ln_); + throw std::runtime_error(buf); +} + +inline std::string trim(const std::string& s) +{ + char p[] = " \t\r\n"; + long sp = 0; + long ep = s.length() - 1; + for (; sp <= ep; ++sp) + if (!strchr(p, s[sp])) break; + for (; ep >= 0; --ep) + if (!strchr(p, s[ep])) break; + return s.substr(sp, ep-sp+1); +} + +inline +Parser::Parser(const char* fn) : f0_(fn), f_(&f0_), ln_(0) +{ + if (!f0_) + throw std::runtime_error(std::string("failed to open file: ") + fn); + + parse(top_); +} + +inline void +Parser::parseSLine(std::string& sname, size_t& depth) +{ + depth = 0; + for (; depth < line_.length(); ++depth) + if (line_[depth] != '[') break; + + sname = line_.substr(depth, line_.length() - 2*depth); +} + +inline void +Parser::parse(Level& l) +{ + while (std::getline(*f_, line_)) { + ++ln_; + if (line_[0] == '#' || line_[0] == ';') continue; + line_ = trim(line_); + if (line_.empty()) continue; + if (line_[0] == '[') { + size_t depth; + std::string sname; + parseSLine(sname, depth); + Level* lp = NULL; + Level* parent = &l; + if (depth > l.depth + 1) + err("section with wrong depth"); + if (l.depth == depth-1) + lp = &l.sections[sname]; + else { + lp = l.parent; + size_t n = l.depth - depth; + for (size_t i = 0; i < n; ++i) lp = lp->parent; + parent = lp; + lp = &lp->sections[sname]; + } + if (lp->depth != 0) + err("duplicate section name on the same level"); + if (!lp->parent) { + lp->depth = depth; + lp->parent = parent; + } + parent->ordered_sections.push_back(parent->sections.find(sname)); + parse(*lp); + } else { + size_t n = line_.find('='); + if (n == std::string::npos) + err("no '=' found"); + std::pair res = + l.values.insert(std::make_pair(trim(line_.substr(0, n)), + trim(line_.substr(n+1, line_.length()-n-1)))); + if (!res.second) + err("duplicated key found"); + l.ordered_values.push_back(res.first); + } + } +} + +inline void +Parser::dump(std::ostream& s, const Level& l, const std::string& sname) +{ + if (!sname.empty()) s << '\n'; + for (size_t i = 0; i < l.depth; ++i) s << '['; + if (!sname.empty()) s << sname; + for (size_t i = 0; i < l.depth; ++i) s << ']'; + if (!sname.empty()) s << std::endl; + for (Level::values_t::const_iterator it = l.ordered_values.begin(); it != l.ordered_values.end(); ++it) + s << (*it)->first << '=' << (*it)->second << std::endl; + for (Level::sections_t::const_iterator it = l.ordered_sections.begin(); it != l.ordered_sections.end(); ++it) { + assert((*it)->second.depth == l.depth+1); + dump(s, (*it)->second, (*it)->first); + } +} + +} + +#endif // INI_HPP + diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/main.cpp b/contrib/frontends/cpp/nntpchan-daemon/src/main.cpp new file mode 100644 index 0000000..ad575f9 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/main.cpp @@ -0,0 +1,78 @@ +#include "ini.hpp" + +#include "storage.hpp" +#include "nntp_server.hpp" +#include "event.hpp" + +#include +#include + + +int main(int argc, char * argv[]) { + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " config.ini" << std::endl; + return 1; + } + + nntpchan::Mainloop loop; + + nntpchan::NNTPServer nntp(loop); + + std::string fname(argv[1]); + + std::ifstream i(fname); + + if(i.is_open()) { + INI::Parser conf(i); + + std::vector requiredSections = {"nntp", "storage"}; + + auto & level = conf.top(); + + for ( const auto & section : requiredSections ) { + if(level.sections.find(section) == level.sections.end()) { + std::cerr << "config file " << fname << " does not have required section: "; + std::cerr << section << std::endl; + return 1; + } + } + + auto & storeconf = level.sections["storage"].values; + + if (storeconf.find("path") == storeconf.end()) { + std::cerr << "storage section does not have 'path' value" << std::endl; + return 1; + } + + nntp.SetStoragePath(storeconf["path"]); + + auto & nntpconf = level.sections["nntp"].values; + + if (nntpconf.find("bind") == nntpconf.end()) { + std::cerr << "nntp section does not have 'bind' value" << std::endl; + return 1; + } + + auto & a = nntpconf["bind"]; + + try { + nntp.Bind(a); + } catch ( std::exception & ex ) { + std::cerr << "failed to bind: " << ex.what() << std::endl; + return 1; + } + try { + std::cerr << "run mainloop" << std::endl; + loop.Run(); + } catch ( std::exception & ex ) { + std::cerr << "exception in mainloop: " << ex.what() << std::endl; + return 2; + } + + } else { + std::cerr << "failed to open " << fname << std::endl; + return 1; + } + + +} diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/message.cpp b/contrib/frontends/cpp/nntpchan-daemon/src/message.cpp new file mode 100644 index 0000000..772540b --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/message.cpp @@ -0,0 +1,29 @@ +#include "message.hpp" + +namespace nntpchan +{ + bool IsValidMessageID(const MessageID & msgid) + { + auto itr = msgid.begin(); + auto end = msgid.end(); + --end; + if (*itr != '<') return false; + if (*end != '>') return false; + bool atfound = false; + while(itr != end) { + auto c = *itr; + ++itr; + if(atfound && c == '@') return false; + if(c == '@') { + atfound = true; + continue; + } + if (c == '$' || c == '_' || c == '-') continue; + if (c > '0' && c < '9') continue; + if (c > 'A' && c < 'Z') continue; + if (c > 'a' && c < 'z') continue; + return false; + } + return true; + } +} diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/message.hpp b/contrib/frontends/cpp/nntpchan-daemon/src/message.hpp new file mode 100644 index 0000000..be4dddf --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/message.hpp @@ -0,0 +1,36 @@ +#ifndef NNTPCHAN_MESSAGE_HPP +#define NNTPCHAN_MESSAGE_HPP + +#include +#include +#include +#include + +namespace nntpchan +{ + typedef std::string MessageID; + + bool IsValidMessageID(const MessageID & msgid); + + typedef std::pair MessageHeader; + + typedef std::map MIMEPartHeader; + + typedef std::function MessageHeaderFilter; + + typedef std::function MIMEPartFilter; + + /** + read MIME message from i, + filter each header with h, + filter each part with p, + store result in o + + return true if we read the whole message, return false if there is remaining + */ + bool StoreMIMEMessage(std::istream & i, MessageHeaderFilter h, MIMEPartHeader p, std::ostream & o); + +} + + +#endif diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/net.cpp b/contrib/frontends/cpp/nntpchan-daemon/src/net.cpp new file mode 100644 index 0000000..6aea582 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/net.cpp @@ -0,0 +1,44 @@ +#include "net.hpp" +#include +#include +#include +#include + +namespace nntpchan +{ + std::string NetAddr::to_string() + { + std::string str("invalid"); + const size_t s = 128; + char * buff = new char[s]; + if(uv_ip6_name(&addr, buff, s) == 0) { + str = std::string(buff); + delete [] buff; + } + std::stringstream ss; + ss << "[" << str << "]:" << ntohs(addr.sin6_port); + return ss.str(); + } + + NetAddr::NetAddr() + { + std::memset(&addr, 0, sizeof(addr)); + } + + NetAddr ParseAddr(const std::string & addr) + { + NetAddr saddr; + auto n = addr.rfind("]:"); + if (n == std::string::npos) { + throw std::runtime_error("invalid address: "+addr); + } + if (addr[0] != '[') { + throw std::runtime_error("invalid address: "+addr); + } + auto p = addr.substr(n+2); + int port = std::atoi(p.c_str()); + auto a = addr.substr(0, n); + uv_ip6_addr(a.c_str(), port, &saddr.addr); + return saddr; + } +} diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/net.hpp b/contrib/frontends/cpp/nntpchan-daemon/src/net.hpp new file mode 100644 index 0000000..42fe4dd --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/net.hpp @@ -0,0 +1,23 @@ +#ifndef NNTPCHAN_NET_HPP +#define NNTPCHAN_NET_HPP + +#include +#include +#include + +namespace nntpchan +{ + struct NetAddr + { + NetAddr(); + + sockaddr_in6 addr; + operator sockaddr * () { return (sockaddr *) &addr; } + operator const sockaddr * () const { return (sockaddr *) &addr; } + std::string to_string(); + }; + + NetAddr ParseAddr(const std::string & addr); +} + +#endif diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/nntp_server.cpp b/contrib/frontends/cpp/nntpchan-daemon/src/nntp_server.cpp new file mode 100644 index 0000000..353e2f9 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/nntp_server.cpp @@ -0,0 +1,132 @@ +#include "buffer.hpp" +#include "nntp_server.hpp" +#include "net.hpp" +#include +#include +#include + +namespace nntpchan +{ + NNTPServer::NNTPServer(uv_loop_t * loop) + { + uv_tcp_init(loop, &m_server); + m_loop = loop; + } + + NNTPServer::~NNTPServer() + { + uv_close((uv_handle_t*)&m_server, [](uv_handle_t *) {}); + } + + void NNTPServer::Bind(const std::string & addr) + { + auto saddr = ParseAddr(addr); + assert(uv_tcp_bind(*this, saddr, 0) == 0); + std::cerr << "nntp server bound to " << saddr.to_string() << std::endl; + m_server.data = this; + auto cb = [] (uv_stream_t * s, int status) { + NNTPServer * self = (NNTPServer *) s->data; + self->OnAccept(s, status); + }; + + assert(uv_listen(*this, 5, cb) == 0); + } + + void NNTPServer::OnAccept(uv_stream_t * s, int status) + { + if(status < 0) { + std::cerr << "nntp server OnAccept fail: " << uv_strerror(status) << std::endl; + return; + } + NNTPServerConn * conn = new NNTPServerConn(m_loop, s, m_storagePath); + conn->SendCode(200, "Posting Allowed"); + } + + + void NNTPServer::SetStoragePath(const std::string & path) + { + m_storagePath = path; + } + + NNTPServerConn::NNTPServerConn(uv_loop_t * l, uv_stream_t * s, const std::string & storage) : + m_handler(storage) + { + uv_tcp_init(l, &m_conn); + m_conn.data = this; + uv_accept(s, (uv_stream_t*) &m_conn); + uv_read_start((uv_stream_t*) &m_conn, [] (uv_handle_t * h, size_t s, uv_buf_t * b) { + NNTPServerConn * self = (NNTPServerConn*) h->data; + b->base = self->m_readbuff; + if (s > sizeof(self->m_readbuff)) + b->len = sizeof(self->m_readbuff); + else + b->len = s; + }, [] (uv_stream_t * s, ssize_t nread, const uv_buf_t * b) { + NNTPServerConn * self = (NNTPServerConn*) s->data; + if(nread > 0) { + self->ProcessData(b->base, nread); + self->SendNextReply(); + } else { + if (nread != UV_EOF) { + std::cerr << "error in nntp server conn alloc: "; + std::cerr << uv_strerror(nread); + std::cerr << std::endl; + } + + delete self; + s->data = nullptr; + + } + }); + } + + NNTPServerConn::~NNTPServerConn() + { + uv_close((uv_handle_t*)&m_conn, [] (uv_handle_t *) {}); + } + + void NNTPServerConn::ProcessData(const char *d, ssize_t l) + { + m_handler.OnData(d, l); + } + + void NNTPServerConn::SendNextReply() + { + if(m_handler.HasNextLine()) { + auto line = m_handler.GetNextLine(); + SendLine(line); + } + } + + void NNTPServerConn::SendCode(const int code, const std::string & msg) + { + std::stringstream ss; + ss << code << " " << msg << std::endl; + SendString(ss.str()); + } + + void NNTPServerConn::SendString(const std::string & line) + { + WriteBuffer * b = new WriteBuffer(line); + uv_write(&b->w, *this, &b->b, 1, [](uv_write_t * w, int status) { + WriteBuffer * wb = (WriteBuffer *) w->data; + delete wb; + }); + } + + NNTPServerHandler::NNTPServerHandler(const std::string & storagepath) : + m_state(eNNTPStateGreet) + { + m_storage.SetPath(storagepath); + } + + NNTPServerHandler::~NNTPServerHandler() + { + } + + void NNTPServerHandler::OnData(const char * d, ssize_t l) + { + + } + +} diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/nntp_server.hpp b/contrib/frontends/cpp/nntpchan-daemon/src/nntp_server.hpp new file mode 100644 index 0000000..ea2fffe --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/nntp_server.hpp @@ -0,0 +1,103 @@ +#ifndef NNTPCHAN_NNTP_SERVER_HPP +#define NNTPCHAN_NNTP_SERVER_HPP +#include +#include +#include +#include "storage.hpp" + +namespace nntpchan +{ + + class NNTPServerConn; + + class NNTPServer + { + public: + NNTPServer(uv_loop_t * loop); + ~NNTPServer(); + + void Bind(const std::string & addr); + + void OnAccept(uv_stream_t * s, int status); + + void SetStoragePath(const std::string & path); + + private: + + operator uv_handle_t * () { return (uv_handle_t*) &m_server; } + operator uv_tcp_t * () { return &m_server; } + operator uv_stream_t * () { return (uv_stream_t *) &m_server; } + + uv_tcp_t m_server; + uv_loop_t * m_loop; + + std::vector m_conns; + + std::string m_storagePath; + + }; + + + class NNTPServerHandler + { + public: + + enum State { + eNNTPStateGreet, + eNNTPStateHandshake, + eNNTPStateReader, + eNNTPStateStream, + eNNTPStateTAKETHIS, + eNNTPStateIHAVE, + eNNTPStateARTICLE, + eNNTPStatePOST + }; + + NNTPServerHandler(const std::string & storagepath); + ~NNTPServerHandler(); + + void OnData(const char * data, ssize_t s); + + bool HasNextLine(); + std::string GetNextLine(); + + private: + State m_state; + ArticleStorage m_storage; + }; + + + class NNTPServerConn + { + public: + NNTPServerConn(uv_loop_t * l, uv_stream_t * s, const std::string & storage); + virtual ~NNTPServerConn(); + + void Close(); + + void Quit(); + + void SendLine(const std::string & line); + void SendCode(const int code, const std::string & message); + + void ProcessData(const char * d, ssize_t l); + + void SendNextReply(); + + private: + + void SendString(const std::string & line); + + + operator uv_stream_t * () { return (uv_stream_t *) &m_conn; } + + uv_tcp_t m_conn; + + NNTPServerHandler m_handler; + + char m_readbuff[1024]; + + }; +} + +#endif diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/storage.cpp b/contrib/frontends/cpp/nntpchan-daemon/src/storage.cpp new file mode 100644 index 0000000..b81e8f5 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/storage.cpp @@ -0,0 +1,42 @@ +#include "storage.hpp" +#include +#include +#include + +namespace nntpchan +{ + ArticleStorage::ArticleStorage() + { + } + + ArticleStorage::~ArticleStorage() + { + } + + void ArticleStorage::SetPath(const std::string & fpath) + { + basedir = fpath; + // quiet fail + // TODO: check for errors + mkdir(basedir.c_str(), 0700); + } + + bool ArticleStorage::Accept(const MessageID & msgid) + { + if (!IsValidMessageID(msgid)) return false; + std::stringstream ss; + ss << basedir << GetPathSep() << msgid; + auto s = ss.str(); + FILE * f = fopen(s.c_str(), "r"); + if ( f == nullptr) return errno == ENOENT; + fclose(f); + return false; + } + + char ArticleStorage::GetPathSep() + { + return '/'; + } + + +} diff --git a/contrib/frontends/cpp/nntpchan-daemon/src/storage.hpp b/contrib/frontends/cpp/nntpchan-daemon/src/storage.hpp new file mode 100644 index 0000000..60e2a69 --- /dev/null +++ b/contrib/frontends/cpp/nntpchan-daemon/src/storage.hpp @@ -0,0 +1,35 @@ +#ifndef NNTPCHAN_STORAGE_HPP +#define NNTPCHAN_STORAGE_HPP + +#include +#include "message.hpp" + +namespace nntpchan +{ + class ArticleStorage + { + public: + ArticleStorage(); + ~ArticleStorage(); + + void SetPath(const std::string & fpath); + + std::ostream & OpenWrite(const MessageID & msgid); + std::istream & OpenRead(const MessageID & msgid); + + /** + return true if we should accept a new message give its message id + */ + bool Accept(const MessageID & msgid); + + private: + + static char GetPathSep(); + + std::string basedir; + + }; +} + + +#endif diff --git a/contrib/frontends/py/README.md b/contrib/frontends/py/README.md new file mode 100644 index 0000000..cacb381 --- /dev/null +++ b/contrib/frontends/py/README.md @@ -0,0 +1,11 @@ +# python nntpchan demo frontend # + +## usage ## + +add to nntpchan.json hooks section: + + { + "name": "pyfront", + "exec": "/path/to/frontend.py" + } + diff --git a/contrib/frontends/py/frontend.py b/contrib/frontends/py/frontend.py new file mode 100644 index 0000000..cdce5e2 --- /dev/null +++ b/contrib/frontends/py/frontend.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import nntpchan +import sys + +if __name__ == "__main__": + msgid = sys.argv[1] + group = sys.argv[2] + if nntpchan.addArticle(msgid, group): + nntpchan.regenerate(msgid, group) diff --git a/contrib/frontends/py/nntpchan/__init__.py b/contrib/frontends/py/nntpchan/__init__.py new file mode 100644 index 0000000..66c9e23 --- /dev/null +++ b/contrib/frontends/py/nntpchan/__init__.py @@ -0,0 +1,42 @@ + +from nntpchan import store +from nntpchan import message +from nntpchan import preprocess + + +def addArticle(msgid, group): + """ + add article to system + :return True if we need to regenerate otherwise False: + """ + msg = None + # open message + with store.openArticle(msgid) as f: + # read article header + hdr = message.readHeader(f) + + mclass = message.MultipartMessage + if hdr.isTextOnly: + # treat as text message instead of multipart + mclass = message.TextMessage + elif hdr.isSigned: + # treat as signed message + mclass = message.TripcodeMessage + # create messgae + msg = mclass(hdr, f) + + if msg is not None: + # we got a message that is valid + store.storeMessage(msg) + else: + # invalid message + print("invalid message: {}".format(msgid)) + return msg is not None + + +def regenerate(msgid, group): + """ + regenerate markup + """ + pass +